/*
 * TiledMap.java
 *
 * Created on 27 avril 2007, 16:44
 *
 */
package courselog.maputils;

import courselog.gpsutils.GeoDirection;
import courselog.gpsutils.GeoPoint;
import ewe.fx.Color;
import ewe.fx.Graphics;
import ewe.fx.IImage;
import ewe.fx.ImageData;
import ewe.fx.Point;
import ewe.fx.Rect;
import ewe.io.File;
import ewe.util.Vector;

/**
 * A virtual map made of several calibrated maps in a given directory.
 *
 * @author rigal
 */
public class TiledMap implements GeoGraphable {

    /** Default scale range == 100.0d */
    public static final double DEFSCALERANGE = 20.0d;
    /** Maximum usage count == 32767 */
    public static final int MAXUSAGECOUNT = 32767;
    /** Vector of maps.
     */
    public Vector myMaps2Use = new Vector();
    /** Array of the used calibrated maps */
    public CalibratedMap myTiledMaps[];
    /** Array of the number of usage for a map. */
    public int myNumberOfUsage[];
    /** Bitmap representation of the map that is used.
     */
    protected IImage imageMap = null;
    /** Image data representation of the map that is used.
     */
    protected ImageData imageDataMap = null;
    /** Maps Min latitude. */
    public double myMinLat;
    /** Maps Max latitude. */
    public double myMaxLat;
    /** Maps Min longitude. */
    public double myMinLon;
    /** Maps Max longitude. */
    public double myMaxLon;
    /** Maps Min scale. */
    public double myMinSc;
    /** Maps Max scale. */
    public double myMaxSc;
    /** Directory name of the calibrated image files.
     */
    public String myDirMapsFileName;
    /** Map Center position
     */
    public MapReferencePoint gpCenter = new MapReferencePoint();
    /** Map average Center position
     */
    public MapReferencePoint gpAverageCenter = new MapReferencePoint();
    /** Center north direction
     */
    public GeoDirection vpNorth = new GeoDirection();
    /** Center east direction
     */
    public GeoDirection vpEast = new GeoDirection();
    /** Map scaling factor. */
    public double dScPixelOnMeters = 1.0d;
    /** Map average scaling factor.
     */
    public double dScAveragePixelOnMeters = 1.0d;
    /** Map upper left corner latitude.
     */
    public double maxLat;
    /** Map upper left corner longitude.
     */
    public double minLon;
    /** Map lower right corner latitude.
     */
    public double minLat;
    /** Map lower right corner longitude.
     */
    public double maxLon;
    /** Computation Map point. */
    public MapReferencePoint tmpMRP = new MapReferencePoint();
    /** Computation rectangle. */
    protected Rect tmpRC = new Rect();

    // CONSTRUCTORS
    /** Creates a new instance from the map name */
    public TiledMap(String imageDirFileName) {
        // TODO ajout d'un mecanisme de Handle pour montrer le chargement
        this.myDirMapsFileName = imageDirFileName;

        File fDirMap = new File(File.fixupPath(myDirMapsFileName));
        if (!fDirMap.isDirectory()) {
//            IllegalArgumentException ex=new IllegalArgumentException(
//                    "No directory file specified");
//            throw ex;
            return;
        }
        String[] dirList = fDirMap.list("*" + CalibratedMap.REFEXTENSION,
                File.LIST_DONT_SORT);
        /*
        String msg="Nb maps : "+dirList.length+"\nfull :"+fDirMap.getFullPath()+"\nfixup "+fDirMap.fixupPath(fDirMap.getFullPath())+"\ndirList "+dirList;
        for(int idx=dirList.length-1; idx>=0; idx--) {
        msg = msg+"fichier "+dirList[idx]+"\n";
        }
        CourseLogAppForm.dbgL.addToLog("\n"+msg);
         */
//        new MessageBox("dir:"+dirList.length,msg,MessageBox.OKB).execute();
        myMaps2Use.setSize(dirList.length);
        for (int idxM = dirList.length - 1; idxM >= 0; idxM--) {
            String fullMapName = File.makePath(fDirMap, dirList[idxM]);
            myMaps2Use.setElementAt(new CalibratedMap(fullMapName), idxM);
        // DOne sort maps in (de ?) increasing scale order
        }
        // DOne calculer centre, largeur, hauteur, echelle placer dans un tableau statique
        myTiledMaps = new CalibratedMap[myMaps2Use.size()];
        myNumberOfUsage = new int[myMaps2Use.size()];
        dScPixelOnMeters = 0.0d;
        gpCenter.lat = 0.0d;
        gpCenter.lon = 0.0d;
        myMaxLat = -1E10d;
        myMinLat = 1E10d;
        myMaxLon = -1E10d;
        myMinLon = 1E10d;
        myMaxSc = 0.0d;
        myMinSc = 1E10d;
        // Put the maps in the vector sorted in ascending scale order
        for (int idxM = myTiledMaps.length - 1; idxM >= 0; idxM--) {
//            double minScale=1e10d; // Maximum scale in meters per pixel
//            int idxMin=-1;
            double maxScale = 0.0d; // Maximum scale in meters per pixel

            int idxMax = -1;
            for (int idxV = myMaps2Use.size() - 1; idxV >= 0; idxV--) {
                CalibratedMap cM = (CalibratedMap) myMaps2Use.elementAt(idxV);
                if (cM.dScPixelOnMeters > maxScale) {
                    idxMax = idxV;
                    maxScale = cM.dScPixelOnMeters;
                }
//                if(cM.dScPixelOnMeters<minScale) {
//                    idxMin=idxV;
//                    minScale=cM.dScPixelOnMeters;
//                }
            }
            if (idxMax == -1) {
                idxMax = myMaps2Use.size() - 1;
            }
            myTiledMaps[idxM] = (CalibratedMap) myMaps2Use.elementAt(idxMax);
            myNumberOfUsage[idxM] = -1;
            /*
            CourseLogAppForm.dbgL.addToLog("\n["+idxM+"]="+
            myTiledMaps[idxM].stMapFileName+
            " sc="+myTiledMaps[idxM].dScPixelOnMeters);
             */
            myMaps2Use.removeElementAt(idxMax); // maybe suboptimal sorting!
//            myTiledMaps[idxM]=(CalibratedMap) myMaps2Use.elementAt(idxMin);
//            myMaps2Use.removeElementAt(idxMin); // maybe suboptimal sorting!

            dScPixelOnMeters += myTiledMaps[idxM].dScPixelOnMeters;
            gpCenter.lat += myTiledMaps[idxM].gpCenter.lat;
            gpCenter.lon += myTiledMaps[idxM].gpCenter.lon;
            if (myTiledMaps[idxM].maxLat > myMaxLat) {
                myMaxLat = myTiledMaps[idxM].maxLat;
            }
            if (myTiledMaps[idxM].minLat < myMinLat) {
                myMinLat = myTiledMaps[idxM].minLat;
            }
            if (myTiledMaps[idxM].maxLon > myMaxLon) {
                myMaxLon = myTiledMaps[idxM].maxLon;
            }
            if (myTiledMaps[idxM].minLon < myMinLon) {
                myMinLon = myTiledMaps[idxM].minLon;
            }
            if (myTiledMaps[idxM].dScPixelOnMeters > myMaxSc) {
                myMaxSc = myTiledMaps[idxM].dScPixelOnMeters;
            }
            if (myTiledMaps[idxM].dScPixelOnMeters < myMinSc) {
                myMinSc = myTiledMaps[idxM].dScPixelOnMeters;
            }
        }
        // Tiled map initial coordinates and scale are the average of its tiles.
        dScPixelOnMeters /= myTiledMaps.length;
        gpCenter.lat /= myTiledMaps.length;
        gpCenter.lon /= myTiledMaps.length;
        gpCenter.altSeaLevel = 0;
        /*
        CourseLogAppForm.dbgL.addToLog("\n"+gpCenter.toString());
         */
//        new MessageBox("center",gpCenter.toString(),MessageBox.OKB).execute();
        setMapCenter(gpCenter);
        gpAverageCenter.copyFrom(gpCenter);
        dScAveragePixelOnMeters = dScPixelOnMeters;
        dScPixelOnMeters = findCenterMaxScale();
    }

    // ACCESSORS
    /**
     * Set the geo reference point to be used as this map center.
     */
    public void setMapCenter(MapReferencePoint gp) {
        gpCenter.copyFrom(gp);
        GeoDirection.vectorsNorthEast(gpCenter, vpNorth, vpEast);
    }

    // CLASS METHODS
    // TODO methode pour liberer la memoire basee sur les statistiques d'usage de chaque tuile
    /**
     * Compute a vector of maps which are partially visible into a graphic rectangle.
     * @todo Make this work for any location (e.g. with |longitude|
     * @todo close to 180° or |latitude| close to 90°
     * @param rD Rectangle to display.
     * @param scaleRange Limit visibility to a scale ratio of scaleRange which
     * shall be >1. If 0<=scaleRange<1, all maps are taken.
     * If scaleRange<0, maps are sorted in ascending scale order
     * @return a vector of calibrated maps supposed to be visible. A subset of myTiledMaps.
     * Maps are sorted in order of scale further to the current scale so that last map 
     * is closer to the current scale. If scaleRange<0, maps are sorted in ascending scale order.
     */
    public Vector findVisibleTiles(Rect rD, double scaleRange) {
        if (myTiledMaps == null) {
            return myMaps2Use;
        }
        GeoPoint ptUL = getPointFromMap(rD.x, rD.y, null);
        GeoPoint ptLR = getPointFromMap(rD.x + rD.width, rD.y + rD.height, null);
        myMaps2Use.setSize(0);
        for (int idxM = myTiledMaps.length - 1; idxM >= 0; idxM--) {
            CalibratedMap cM = myTiledMaps[idxM];
            boolean isMapUsed = false;
            if ((cM.maxLat >= ptLR.lat) && (cM.minLat <= ptUL.lat)) {
                // Latitude interval should be visible
                if ((cM.maxLon >= ptUL.lon) && (cM.minLon <= ptLR.lon)) {
                    // Longitude interval is visible too
                    double rScales;
                    if (cM.dScPixelOnMeters > this.dScPixelOnMeters) {
                        rScales = cM.dScPixelOnMeters / this.dScPixelOnMeters;
                    } else {
                        rScales = this.dScPixelOnMeters / cM.dScPixelOnMeters;
                    } // rScales is a scale ratio >1

                    // Select map if its scale is not too different from the current one

                    if ((scaleRange <= 1.0d) || (rScales <= scaleRange)) {
                        isMapUsed = true;
                        // Map is usable, insert it in the vector
                        if (scaleRange < 0) {
                            myMaps2Use.add(cM);
                        } else {
                            // closer to the end so that it is drawn last when
                            // its scale is closer to the current TiledMap scale.
                            int idxMV = myMaps2Use.size() - 1;
                            while (idxMV >= 0) {
                                double rScM = ((CalibratedMap) myMaps2Use.elementAt(idxMV)).dScPixelOnMeters;
                                if (rScM > this.dScPixelOnMeters) {
                                    rScM = rScM / this.dScPixelOnMeters;
                                } else {
                                    rScM = this.dScPixelOnMeters / rScM;
                                } // rScM is the map at idxMV scale ratio >1
                                if (rScales < rScM) {
                                    // There is a map closer to scale 1
                                    myMaps2Use.insertElementAt(cM, idxMV + 1);
                                    idxMV = -2; // Flags the map has been inserted

                                }
                                idxMV--;
                            }
                            if (idxMV == -1) {
                                // No map is further to scale 1
                                myMaps2Use.insertElementAt(cM, 0);
                            }
                        }
                    }
                }
            }
            if (isMapUsed) {
                if (myNumberOfUsage[idxM] < MAXUSAGECOUNT) {
                    myNumberOfUsage[idxM]++;
                }
            } else {
                if (myNumberOfUsage[idxM] > 0) {
                    myNumberOfUsage[idxM]--;
                }
            }
        }
        return myMaps2Use;
    }

    /**
     * Release maps which are not used.
     */
    public void releaseUnusedMaps() {
        for (int idx = myNumberOfUsage.length - 1; idx >= 0; idx--) {
            if (myNumberOfUsage[idx] == 0) {
                myTiledMaps[idx].free();
                myNumberOfUsage[idx] = 0;
            } else {
                if (myNumberOfUsage[idx] > 0) {
                    myNumberOfUsage[idx] = 1;
                }
            }
        }
    }

    /**
     * Find the maximum scale at the current map center.
     */
    public double findCenterMaxScale() {
        tmpRC.x = gpCenter.iX;
        tmpRC.y = gpCenter.iY;
        tmpRC.width = 0;
        tmpRC.height = 0;
        Vector vV = findVisibleTiles(tmpRC, -1.0d);
        if (vV.size() > 0) {
            return ((CalibratedMap) vV.elementAt(vV.size() - 1)).dScPixelOnMeters;
        } else {
            return dScAveragePixelOnMeters;
        }
    }

    /**
     * Find the closer scale at the current map center.
     */
    public double findCenterCloserScale() {
        tmpRC.x = gpCenter.iX;
        tmpRC.y = gpCenter.iY;
        tmpRC.width = 0;
        tmpRC.height = 0;
        Vector vV = findVisibleTiles(tmpRC, 0.0d);
        if (vV.size() > 0) {
            return ((CalibratedMap) vV.elementAt(vV.size() - 1)).dScPixelOnMeters;
        } else {
            return dScAveragePixelOnMeters;
        }
    }

    /**
     * Find the smaller scale at the current map center.
     */
    public double findCenterMinScale() {
        tmpRC.x = gpCenter.iX;
        tmpRC.y = gpCenter.iY;
        tmpRC.width = 0;
        tmpRC.height = 0;
        Vector vV = findVisibleTiles(tmpRC, -1.0d);
        if (vV.size() > 0) {
            return ((CalibratedMap) vV.elementAt(0)).dScPixelOnMeters;
        } else {
            return dScAveragePixelOnMeters;
        }
    }

    /**
     * Limit displacement of the center of map, in a given rectangle.
     */
    public Point moveCenterLimited(Point ptMove, Rect rFrame) {
        // Center has to moved find new center of the tiled map.
        int cX = rFrame.width / 2 - ptMove.x;
        int cY = rFrame.height / 2 - ptMove.y;
        tmpMRP = (MapReferencePoint) getPointFromMap(cX, cY, tmpMRP);
        boolean wasChanged = false;
        if (tmpMRP.lat < myMinLat) {
            // Too far in the negative lat direction
            tmpMRP.lat = myMinLat;
            wasChanged = true;
        }
        if (tmpMRP.lat > myMaxLat) {
            // Too far in the positive lat direction
            tmpMRP.lat = myMaxLat;
            wasChanged = true;
        }
        if (tmpMRP.lon < myMinLon) {
            // Too far in the negative lon direction
            tmpMRP.lon = myMinLon;
            wasChanged = true;
        }
        if (tmpMRP.lon > myMaxLon) {
            // Too far in the positive lon direction
            tmpMRP.lon = myMaxLon;
            wasChanged = true;
        }
        if (wasChanged) {
            // recompute the map move
            tmpMRP.computeCartesian(); // Recompute position coordinates

            ptMove = getMapPointFromPosition(tmpMRP, null, ptMove);
            ptMove.x = rFrame.width / 2 - ptMove.x;
            ptMove.y = rFrame.height / 2 - ptMove.y;
        }
        // remain at the center of the display
        tmpMRP.iX = gpCenter.iX;
        tmpMRP.iY = gpCenter.iY;
        setMapCenter(tmpMRP);
        return ptMove;
    }

    /** Free image data */
    public void free() {
        if (this.myTiledMaps == null) {
            return;
        }
        for (int idxM = myTiledMaps.length; idxM >= 0; idxM--) {
            myTiledMaps[idxM].free();
        }
    }

    /**
     * Compute lat and long from position on map.
     */
    public GeoPoint getPointFromMap(int xx, int yy, GeoPoint dest) {
        // Position relative to image center
        xx -= gpCenter.iX;
        yy = gpCenter.iY - yy;
        // Move center point by xx and yy - dest is allocated if null
        dest = gpCenter.moveBy(true, vpEast, xx / dScPixelOnMeters,
                vpNorth, yy / dScPixelOnMeters, dest);

        return dest;
    }

    /**
     * Compute X and Y on map from geo position.
     * @param pos geo position to transpose.
     * @param tmp GeoDirection reference to compute intermediate direction.
     * If <I><B>null</B></I> a new object is allocated.
     * @param pt point on bitmap image to compute.
     * If <I><B>null</B></I> a new Point object is allocated.
     * @return Computed point. Should be <I>pt</I> or a new Point object.
     */
    public Point getMapPointFromPosition(GeoPoint pos, GeoDirection tmp,
            Point pt) {
        // Allocate point if needed
        if (pt == null) {
            pt = new Point();
        }

        // Direction
        tmp = this.gpCenter.directionTo(pos, tmp);
        // Scalar product with east vector
        double dX = tmp.x * this.vpEast.x + tmp.y * vpEast.y + tmp.z * vpEast.z;
        double dY = tmp.x * this.vpNorth.x + tmp.y * vpNorth.y + tmp.z * vpNorth.z;

        pt.x = (int) (dX * dScPixelOnMeters + gpCenter.iX + 0.5d);
        pt.y = (int) (gpCenter.iY - dY * dScPixelOnMeters + 0.5d);

        return pt;
    }

    /**
     * Draws the computed image of this map into a graphic rectangle.
     * The operation works provided that the center of the tiled map has been
     * properly initialised with setCenterInImage(x,y).
     */
    public void drawIn(Graphics g, Rect r, Color bgC) {
        g.setColor(bgC);
        g.setDrawOp(Graphics.DRAW_OVER);
        g.fillRect(r.x, r.y, r.width, r.height);

        Vector vT = findVisibleTiles(r, DEFSCALERANGE);
        Point pTUL = new Point(); // Upper Left point on map in Tiled coordinates

        Point pTLR = new Point(); // Lower Right point on map in Tiled coordinates

        Point pCUL = new Point(); // Upper Left point on map in Calibrated coordinates

        Point pCLR = new Point(); // Lower Right point on map in Calibrated coordinates

        GeoPoint gRUL = new GeoPoint(); // Upper left position of rectangle

        GeoPoint gRLR = new GeoPoint(); // Lower Right position of rectangle

        GeoPoint gMUL = new GeoPoint(); // Upper left position of Map rectangle

        GeoPoint gMLR = new GeoPoint(); // Lower Right position of Map rectangle

        GeoDirection gdTmp = null;
        boolean isULModified;
        boolean isLRModified;
        Rect rS = new Rect();
        Rect rD = new Rect();
        // Get geo positions of Upper left and lower right of rectangle r
        gRUL = getPointFromMap(r.x - 1, r.y - 1, gRUL);
        gRLR = getPointFromMap(r.x + r.width + 1, r.y + r.height + 1, gRLR);
        // Loop on visible sub-maps, sorted by scaling order so that the
        // maps having a less adapted scale appear first in the vector
        int vTSize = vT.size();
        for (int idxM = 0; idxM < vTSize; idxM++) {
            CalibratedMap cM = (CalibratedMap) vT.elementAt(idxM);
            isULModified = false;
            pCUL = cM.getMapPointFromPosition(gRUL, gdTmp, pCUL);
            if (pCUL.x > cM.imgW) {
                continue; // outside this map, don't use it.

            }
            if (pCUL.y > cM.imgH) {
                continue; // outside this map, don't use it.

            }
            if (pCUL.x < 0) {
                isULModified = true;
                pCUL.x = 0;
            }
            if (pCUL.y < 0) {
                isULModified = true;
                pCUL.y = 0;
            }
            if (isULModified) {
                gMUL = cM.getPointFromMap(pCUL.x, pCUL.y, gMUL);
                pTUL = getMapPointFromPosition(gMUL, gdTmp, pTUL);
            } else {
                pTUL.set(r.x, r.y);
            }
            isLRModified = false;
            pCLR = cM.getMapPointFromPosition(gRLR, gdTmp, pCLR);
            if (pCLR.x < 0) {
                continue; // outside this map, don't use it.

            }
            if (pCLR.y < 0) {
                continue; // outside this map, don't use it.

            }
            if (pCLR.x > cM.imgW) {
                isLRModified = true;
                pCLR.x = cM.imgW;
            }
            if (pCLR.y > cM.imgH) {
                isLRModified = true;
                pCLR.y = cM.imgH;
            }
            if (isLRModified) {
                gMLR = cM.getPointFromMap(pCLR.x, pCLR.y, gMLR);
                pTLR = getMapPointFromPosition(gMLR, gdTmp, pTLR);
            } else {
                pTLR.set(r.width + r.x, r.height + r.y);
            }
            rS.set(pCUL.x, pCUL.y, pCLR.x - pCUL.x, pCLR.y - pCUL.y);
            rD.set(pTUL.x, pTUL.y, pTLR.x - pTUL.x, pTLR.y - pTUL.y);
            // Draw on graphics
            if ((rS.width > 0) && (rS.height > 0) && (rD.width > 0) && (rD.height > 0)) {
                g.drawImage(cM.getImageMap(), null, null, rS, rD, 0);
            }
        }
    }
}